package org.springframework.data.jpa.repository.query;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import javax.persistence.EntityManager;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.provider.QueryExtractor;
import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.Param;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;

import spring.data.jpa.freemarker.FreemarkerResourceContext;
import spring.data.jpa.repository.support.template.TemplateParam;
import spring.data.jpa.repository.support.template.TemplateQuery;
import spring.data.jpa.repository.support.template.TemplateSupport;

import com.slyak.spring.jpa.FreemarkerSqlTemplates;
import com.slyak.spring.jpa.NamedTemplateResolver;

/**
 * <p>描述: 支持模板查询的Query适配器
 * <p>日期: 2020年1月10日 下午2:27:05
 * <p>作者: wangjie
 */

public class TemplateQueryAdaper implements RepositoryQuery {
    private static final Log logger = LogFactory.getLog(TemplateQueryAdaper.class);

    private final Method method;
    private final EntityManager em;
    private final QueryMethodEvaluationContextProvider evaluationContextProvider;
    private final JpaQueryMethod jpaQueryMethod;

    private String[] templates;

    public TemplateQueryAdaper(Method method, EntityManager em,
            QueryMethodEvaluationContextProvider evaluationContextProvider, RepositoryMetadata metadata,
            ProjectionFactory factory, QueryExtractor extractor, FreemarkerResourceContext freemarkerContext) {
        this.method = method;
        this.em = em;
        this.evaluationContextProvider = evaluationContextProvider;
        this.jpaQueryMethod = new TemplateQueryMethod(method, metadata, factory, extractor);
        Class<?> domainType = metadata.getDomainType();
        TemplateSupport ts = AnnotationUtils.findAnnotation(domainType, TemplateSupport.class);
        if (ts != null) {
            try {
                NamedTemplateResolver resolver = ts.resolver().newInstance();
                this.templates = ts.name().length == 0 ? new String[] { domainType.getSimpleName()
                        + resolver.getSuffix() } : ts.name();
                FreemarkerSqlTemplates.addTemplate(StringUtils.join(templates, "#"), resolver, freemarkerContext);
            } catch (InstantiationException | IllegalAccessException e) {
                throw new UnsupportedOperationException("模板解析器实例化失败", e);
            }
        }
    }

    @Override
    public Object execute(Object[] values) {
        TemplateQuery tq = AnnotationUtils.findAnnotation(method, TemplateQuery.class);
        Map<String, Object> templateModelData = getTemplateModelData(values);
        String queryString = null;
        if (tq == null) {
            throw new IllegalArgumentException("没有找到TemplateQuery注解，无法执行按模板查询");
        }
        String queryName = tq.name();
        if (StringUtils.isBlank(queryName)) {
            queryName = method.getName();
        }
        queryString = FreemarkerSqlTemplates.getTemplate(StringUtils.join(templates, "#")).process(templates,
                queryName, templateModelData);
        AbstractJpaQuery jpaQuery = JpaQueryFactory.INSTANCE.fromMethodWithQueryString(jpaQueryMethod, em, queryString,
                evaluationContextProvider);
        Object[] paramValues = getQueryData(values);
        return jpaQuery.execute(paramValues);
    }

    private Map<String, Object> getTemplateModelData(Object[] values) {
        Map<String, Object> paramNames = new HashMap<>(values.length);
        for (int i = 0; i < values.length; i++) {
            MethodParameter methodParam = new MethodParameter(method, i);
            Annotation[] paramAnns = methodParam.getParameterAnnotations();

            for (Annotation paramAnn : paramAnns) {
                if (Param.class.isInstance(paramAnn)) {
                    Param templateParam = (Param) paramAnn;
                    paramNames.put(templateParam.value(), values[i]);
                } else if (TemplateParam.class.isInstance(paramAnn)) {
                    TemplateParam templateParam = (TemplateParam) paramAnn;
                    paramNames.put(templateParam.value(), values[i]);
                }
            }
        }
        return paramNames;
    }

    /**
     * 参数的匹配逻辑为：
     * <p>1.根据sql解析绑定的参数名，即sql中以:开始的参数名
     * <p>2.根据参数名遍历方法上传入的参数，方法参数由{@code TemplateJpaParameter}解析出对应的参数名，参数绑定器 {@link org.springframework.data.jpa.repository.query.QueryParameterSetterFactory.BasicQueryParameterSetterFactory#create(org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding, DeclaredQuery) BasicQueryParameterSetterFactory} 根据匹配的参数名找到对应的参数位置parameterIndex 
     * <p>3.通过上面找到的参数位置，找到values中的参数
     * <p> 因此这里的参数解析需要和{@code TemplateParameterNameDiscoverer}中的参数名称解析顺序一致
     */
    private Object[] getQueryData(Object[] values) {
        List<Object> paramList = new ArrayList<>();
        for (int i = 0; i < values.length; i++) {
            Object object = values[i];
            if (isAssignable(object.getClass())) {
                paramList.add(object);
            } else {
                List<String> fieldNames = getObjectParameterNames(object.getClass());
                for (String fieldName : fieldNames) {
                    try {
                        Field field = ReflectionUtils.findField(object.getClass(), fieldName);
                        ReflectionUtils.makeAccessible(field);
                        paramList.add(ReflectionUtils.getField(field, object));
                    } catch (IllegalArgumentException e) {
                        logger.error("access object[" + object.getClass().getName() + "] field[" + fieldName
                                + "] value fail");
                    }
                }
            }
        }
        return paramList.toArray(new Object[paramList.size()]);
    }

    @Override
    public QueryMethod getQueryMethod() {
        return jpaQueryMethod;
    }

    static boolean isAssignable(Class<?> clazz) {
        return Collection.class.isAssignableFrom(clazz) || clazz.isArray() || Number.class.isAssignableFrom(clazz)
                || String.class.isAssignableFrom(clazz) || Date.class.isAssignableFrom(clazz)
                || Character.class.isAssignableFrom(clazz) || Pageable.class.isAssignableFrom(clazz)
                || Sort.class.isAssignableFrom(clazz);
    }
    
    static List<String> getObjectParameterNames(Class<?> clazz, String... prefix) {
        Field[] fields = clazz.getDeclaredFields();
        List<String> names = new ArrayList<>(fields.length);
        String preStr = "";
        if (ArrayUtils.isNotEmpty(prefix)) {
            //TODO jpql 中 “.”有特殊的含义，不支持
            preStr = prefix[0] + ".";
        }
        for (Field field : fields) {
            if (!field.getName().equals("serialVersionUID")) {
                String name = preStr + field.getName();
                names.add(name);
            }
        }
        return names;
    }

    /**
     * <p>描述: 自定义模板参数识别器，可识别TemplateParam注解的参数
     * <p> {@link org.springframework.data.jpa.repository.query.ParameterBinderFactory#createQueryAwareBinder(JpaParameters, DeclaredQuery, org.springframework.expression.spel.standard.SpelExpressionParser, QueryMethodEvaluationContextProvider)}
     * <p>日期: 2020年1月10日 下午2:30:53
     * <p>作者: wangjie
     */
    static class TemplateParameterNameDiscoverer extends DefaultParameterNameDiscoverer {

        public TemplateParameterNameDiscoverer() {
            super();
            addDiscoverer(new ParameterNameDiscoverer() {

                @Override
                public String[] getParameterNames(Constructor<?> ctor) {
                    throw new UnsupportedOperationException();
                }

                @Override
                public String[] getParameterNames(Method method) {
                    return this.getTemplateParameterNames(method);
                }

                @Nullable
                private String[] getTemplateParameterNames(Executable executable) {
                    List<String> parameterNames = new ArrayList<>();
                    Annotation[][] annotationArray = executable.getParameterAnnotations();
                    Class<?>[] parameters = executable.getParameterTypes();

                    for (int i = 0; i < annotationArray.length; i++) {
                        Annotation[] parAnnos = annotationArray[i];
                        for (int j = 0; j < parAnnos.length; j++) {
                            Annotation paramAnn = parAnnos[j];
                            if (Param.class.isInstance(paramAnn)) {
                                Param param = (Param) paramAnn;
                                parameterNames.add(param.value());
                            } else if (TemplateParam.class.isInstance(paramAnn) && isAssignable(parameters[i])) {
                                TemplateParam templateParam = (TemplateParam) paramAnn;
                                parameterNames.add(templateParam.value());
                            } else {
                                TemplateParam templateParam = (TemplateParam) paramAnn;
                                parameterNames.addAll(getObjectParameterNames(parameters[i], templateParam.value()));
                            }
                        }
                    }
                    return parameterNames.toArray(new String[parameterNames.size()]);
                }
            });
        }

    }

    /**
     * <p>描述: 支持模板查询的方法的包装类，用于识别TemplateParam注解的jpa查询参数
     * <p>日期: 2020年1月10日 下午2:29:19
     * <p>作者: wangjie
     */
    static class TemplateQueryMethod extends JpaQueryMethod {

        private Method method;

        public TemplateQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
                QueryExtractor extractor) {
            super(method, metadata, factory, extractor);
            this.method = method;
        }

        @Override
        boolean isNativeQuery() {
            Annotation annotation = AnnotatedElementUtils.findMergedAnnotation(method, TemplateQuery.class);
            if (annotation == null) {
                return false;
            }
            return (boolean) AnnotationUtils.getValue(annotation, "nativeQuery");
        }

        /**
         * 由父类 org.springframework.data.repository.query.QueryMethod 构造函数中调用
         * @see org.springframework.data.jpa.repository.query.JpaQueryMethod#createParameters(java.lang.reflect.Method)
         */
        @Override
        protected JpaParameters createParameters(Method method) {
            TemplateParameters jpaParameters = new TemplateParameters(method);
            int numOfParameters = jpaParameters.getNumberOfParameters();
            if (numOfParameters == 0) {
                return jpaParameters;
            }
            try {
                Constructor<?> constructor = JpaParameters.class.getDeclaredConstructor(List.class);
                List<JpaParameter> parameters = new ArrayList<>();
                int parameterIndex = 0;
                for (int i = 0; i < numOfParameters; i++) {
                    TemplateJpaParameter parameter = (TemplateJpaParameter) jpaParameters.getParameter(i);
                    if (isAssignable(parameter.getType())) {
                        parameter.setIndex(parameterIndex);
                        parameters.add(parameter);
                        parameterIndex++;
                    } else {
                        List<String> fieldNames = getObjectParameterNames(parameter.getType());
                        for (int j = 0; j < fieldNames.size(); j++) {
                            MethodParameter methodParameter = new MethodParameter(method, i);
                            TemplateJpaParameter templateJpaParameter = (TemplateJpaParameter) jpaParameters
                                    .createParameter(methodParameter);
                            templateJpaParameter.setIndex(parameterIndex);
                            parameters.add(templateJpaParameter);
                            parameterIndex++;
                        }
                    }
                }
                constructor.setAccessible(true);
                return (JpaParameters) constructor.newInstance(parameters);
            } catch (Exception e) {
                logger.error("Create parameters error with method[" + method.getName() + "]");
            }

            return jpaParameters;
        }

        static class TemplateParameters extends JpaParameters {

            public TemplateParameters(Method method) {
                super(method);
            }

            @Override
            protected JpaParameter createParameter(MethodParameter parameter) {
                return new TemplateJpaParameter(parameter);
            }
        }

        static class TemplateJpaParameter extends JpaParameters.JpaParameter {
            private static final ParameterNameDiscoverer DISCOVERER = new TemplateParameterNameDiscoverer();

            private int parameterIndex;
            private final MethodParameter parameter;

            TemplateJpaParameter(MethodParameter parameter) {
                super(parameter);
                this.parameter = parameter;
                this.parameterIndex = parameter.getParameterIndex();
            }

            public void setIndex(int parameterIndex) {
                this.parameterIndex = parameterIndex;
            }

            @Override
            public int getIndex() {
                return this.parameterIndex;
            }

            @Override
            public Optional<String> getName() {
                Param annotation = parameter.getParameterAnnotation(Param.class);
                if (annotation != null) {
                    return Optional.of(annotation.value());
                }
                String[] parameterNames = DISCOVERER.getParameterNames(parameter.getMethod());
                String parameterName = ArrayUtils.isNotEmpty(parameterNames) ? parameterNames[parameterIndex] : null;
                return Optional.ofNullable(parameterName);
            }

            @Override
            public boolean isExplicitlyNamed() {
                return parameter.hasParameterAnnotation(Param.class)
                        || parameter.hasParameterAnnotation(TemplateParam.class);
            }
        }

    }
}
